﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Medianamik.Core.DAL.SQL;
using Medianamik.Core.DAL.SQL.Clauses;
using Medianamik.Core.Querying;
using Medianamik.Core.Interfaces;

namespace Medianamik.Core.DAL
{
    public class RowNumberQueryBuilder
    {
        protected IQueryDefinition QueryDefinition
        {
            get; private set;
        }

        protected ParameterNameGenerator ParamGenerator
        {
            get; private set;
        }

        protected JoinNameGenerator JoinGenerator
        {
            get; private set;
        }

        public RowNumberQueryBuilder(IQueryDefinition pQueryDef, 
            ParameterNameGenerator pParamGenerator, JoinNameGenerator pJoinGenerator)
        {
            QueryDefinition = pQueryDef;
            ParamGenerator = pParamGenerator;
            JoinGenerator = pJoinGenerator;
        }

        public RowNumberQuery Build(out IEnumerable<IParameter> pParams, bool withDeleted)
        {
            var queryParams = new List<IParameter>();

            var nodeTable = new NodeTable("n");

            var orderByColumns = QueryDefinition.NodeOrderByCollection
                .ToDictionary(member => member.GetColumn(nodeTable), 
                    member => member.Descending);

            if (orderByColumns.Count == 0)
            {
                orderByColumns.Add(nodeTable["Position"], false);
                orderByColumns.Add(nodeTable["CreatedOn"], true);
            }

            var orderByClause = new OrderByClause();

            foreach (var col in orderByColumns.Keys)
            {
                if (orderByColumns[col])
                {
                    orderByClause.OrderByDesc(col);
                }
                else
                {
                    orderByClause.OrderBy(col);
                }
            }

            var selectClause = new RowNumberSelectClause(orderByClause, nodeTable["NodeId"]);
            var fromClause = new FromClause(nodeTable);
            var whereClause = new WhereClause();

            if (!withDeleted)
                whereClause.AddPredicate(new ColumnPredicate(nodeTable["IsDeleted"], 
                    Comparison.Equal, new NumericExpression(0)));

            var groupByClause = new GroupByClause()
                .AddColumns(orderByColumns.Keys.Union(new[] { nodeTable["NodeId"] }));

            AddNodeWheres(nodeTable, whereClause, queryParams);
            AddNodeTypeWheres(nodeTable, whereClause, queryParams, fromClause);
            AddPropertyWheres(nodeTable, whereClause, queryParams, fromClause);

            if (QueryDefinition.ModificationCulture != null)
            {
                var nodeInstanceTable = new NodeInstanceTable(JoinGenerator.Next());
                fromClause.InnerJoin(nodeInstanceTable["NodeId"], nodeTable["NodeId"]);
                var cultureParam = new Parameter(ParamGenerator.Next(), 
                    QueryDefinition.ModificationCulture.Name);
                whereClause.AddPredicate(new ColumnPredicate(nodeInstanceTable["CultureName"], 
                    Comparison.Equal, new ParameterExpression(cultureParam)));
                whereClause.AddPredicate(new ColumnPredicate(nodeInstanceTable["HasModifications"], 
                    Comparison.Equal, new NumericExpression(true)));
                queryParams.Add(cultureParam);
            }

            var rowNumberQuery = new RowNumberQuery(selectClause, fromClause, whereClause, groupByClause);
            pParams = queryParams;

            return rowNumberQuery;
        }

        private void AddNodeWheres(NodeTable nodeTable, WhereClause whereClause, 
            List<IParameter> queryParams)
        {
            foreach (var nWhere in QueryDefinition.NodeWheres)
            {
                var col = nWhere.GetColumn(nodeTable);

                IExpression expression;

                if (nWhere.Value is IEnumerable
                    && !(nWhere.Value is string))
                {
                    var values = ((IEnumerable)nWhere.Value).Cast<Object>();
                    var parameters = values.Select(v => 
                        (IParameter)new Parameter(ParamGenerator.Next(), v)).ToList();
                    expression = new ParameterCollectionExpression(parameters);
                    queryParams.AddRange(parameters);
                }
                else
                {
                    var parameter = new Parameter(ParamGenerator.Next(), nWhere.Value);
                    expression = new ParameterExpression(parameter);
                    queryParams.Add(parameter);
                }

                var predicate = new ColumnPredicate(col, nWhere.Comparison, expression);
                whereClause.AddPredicate(predicate);
            }
        }

        private void AddNodeTypeWheres(NodeTable nodeTable, WhereClause whereClause, 
            List<IParameter> queryParams, FromClause fromClause)
        {
            var nodeTypeTable = new NodeTypeTable(JoinGenerator.Next());

            if (QueryDefinition.NodeTypeWheres.Count > 0)
            {
                fromClause.InnerJoin(nodeTypeTable["TypeId"], nodeTable["TypeId"]);
            }

            foreach (var tWhere in QueryDefinition.NodeTypeWheres)
            {
                var col = tWhere.GetColumn(nodeTypeTable);
                IExpression expression;

                if (tWhere.Value is IEnumerable
                    && !(tWhere.Value is string))
                {
                    var values = ((IEnumerable)tWhere.Value).Cast<Object>();
                    var parameters = values.Select(v => 
                        (IParameter)new Parameter(ParamGenerator.Next(), v)).ToList();
                    expression = new ParameterCollectionExpression(parameters);
                    queryParams.AddRange(parameters);
                }
                else
                {
                    var parameter = new Parameter(ParamGenerator.Next(), tWhere.Value);
                    expression = new ParameterExpression(parameter);
                    queryParams.Add(parameter);
                }

                var predicate = new ColumnPredicate(col, tWhere.Comparison, expression);
                whereClause.AddPredicate(predicate);
            }
        }

        private void AddPropertyWheres(NodeTable nodeTable, WhereClause whereClause, 
            List<IParameter> queryParams, FromClause fromClause)
        {
            foreach (var pWhere in QueryDefinition.PropertyWheres)
            {
                var table = new NodePropertyValueTable(JoinGenerator.Next());

                fromClause.InnerJoin(table["NodeId"], nodeTable["NodeId"]);
                fromClause.CrossApply(new XmlCrossApplyClause(table["Data"], "/root/properties/property"));

                var propertyIdParam = new Parameter(ParamGenerator.Next(), pWhere.Property.ID);
                queryParams.Add(propertyIdParam);
                var propertyIdExpr = new ParameterExpression(propertyIdParam);
                var propertyIdPredicate = new XmlColumnPredicate("id", 
                    Comparison.Equal, propertyIdExpr, "varchar(36)");
                whereClause.AddPredicate(propertyIdPredicate);

                IExpression valueExpr;

                if (pWhere.Value is IEnumerable
                    && !(pWhere.Value is string))
                {
                    var values = ((IEnumerable)pWhere.Value).Cast<object>();
                    var parameters = values.Select(v => 
                        (IParameter)new Parameter(ParamGenerator.Next(), v)).ToList();
                    valueExpr = new ParameterCollectionExpression(parameters);
                    queryParams.AddRange(parameters);
                }
                else
                {
                    var parameter = new Parameter(ParamGenerator.Next(), pWhere.Value);
                    valueExpr = new ParameterExpression(parameter);
                    queryParams.Add(parameter);
                }

                var valuePredicate = new XmlColumnPredicate("value", pWhere.Comparison, valueExpr, "nvarchar(max)");
                whereClause.AddPredicate(valuePredicate);
            }
        }
    }
}
